// Copyright (c) 2012 SameGoal LLC.
// All Rights Reserved.
// Author: Andy Hochhaus <ahochhaus@samegoal.com>

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "net/http/server/event/proxy_request.h"

#include <string>

#include "base/string.h"
#include "gflags/gflags.h"
#include "glog/logging.h"
#include "event2/buffer.h"
#include "event2/bufferevent.h"
#include "event2/event.h"
#include "event2/listener.h"
#include "event2/util.h"
#include "net/http/server/event/event_server.h"
#include "net/http/server/event/request.h"

namespace sg {
namespace net {
namespace http {
namespace server {
namespace event {

const uint32 kTimeout = 5 * 60 * 1000;

ProxyRequest::ProxyRequest(Request *req, EventServer *event_server)
    : req_(req), event_server_(event_server), bev_(NULL) {
  timeout_.tv_sec = kTimeout / 1000;
  timeout_.tv_usec = (kTimeout % 1000) * 1000;
}

ProxyRequest::~ProxyRequest() {
  bufferevent_free(bev_);
}

void ProxyRequest::To(const std::string &host, const uint16 port) {
  bev_ = bufferevent_socket_new(
      event_server_->event_base(),
      -1,
      BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE | BEV_OPT_DEFER_CALLBACKS);
  CHECK_NOTNULL(bev_);
  bufferevent_setcb(bev_, &ReadCallback, NULL, &EventCallback, this);
  bufferevent_set_timeouts(bev_, &timeout_, NULL);
  bufferevent_socket_connect_hostname(bev_,
                                      NULL,
                                      AF_INET,
                                      host.c_str(),
                                      port);
  bufferevent_enable(bev_, EV_READ);
  std::string request_str = req_->GetRawRequest();
  if (bufferevent_write(bev_,
                        request_str.c_str(),
                        request_str.length()) != 0) {
    LOG(ERROR) << "unable to write output to bev_";
    req_->SendError(INTERNAL_SERVER_ERROR, "Unable to proxy request.");
    delete this;
    return;
  }
}

void ProxyRequest::ReadCallback(bufferevent *bev, void *ctx) {
  ProxyRequest *proxy_request = static_cast<ProxyRequest *> (ctx);
  CHECK_NOTNULL(proxy_request);

  struct evbuffer *input_buf = bufferevent_get_input(bev);
  uint32 len = evbuffer_get_length(input_buf);
  uint8 *input_ptr = evbuffer_pullup(input_buf, len);
  std::string input_str(reinterpret_cast<char *> (input_ptr), len);

  const uint64 header_end_offset = input_str.find("\r\n\r\n");
  if (header_end_offset == std::string::npos) {
    return;
  }

  std::string response_code;
  if (sg::string::StartsWith(input_str, "HTTP/1.0 ")) {
    response_code = ExtractValue(input_str, "HTTP/1.0 ");
  } else {
    response_code = ExtractValue(input_str, "HTTP/1.1 ");
  }
  const std::string te = ExtractValue(input_str, "Transfer-Encoding: ");
  const std::string cl_str = ExtractValue(input_str, "Content-Length: ");
  if (response_code == "304") {
    // response must not have a body
  } else if (te == "chunked") {
    uint64 offset = header_end_offset + 4;
    uint64 chunk_length = 1;
    while (chunk_length != 0) {
      uint64 chunk_prefix_bytes;
      if (!ExtractChunkLength(
              input_str, offset, &chunk_prefix_bytes, &chunk_length)) {
        return;
      }
      offset += chunk_length + chunk_prefix_bytes + 2;
      if (offset > input_str.length()) {
        return;
      }
    }
  } else if (!cl_str.empty()) {
    uint64 cl = 0;
    if (!sg::string::FromString(cl_str, &cl)) {
      LOG(WARNING) << "unable to parse content-length: " << cl_str
                   << " waiting...";
      return;
    }
    const uint64 expected_length = cl + header_end_offset + 4;
    if (len < expected_length) {
      return;
    }
  } else {
    LOG(WARNING) << "unable to determine http request end. waiting...";
    return;
  }

  proxy_request->req_->SendRaw(input_str);
  delete proxy_request;
  return;
}

void ProxyRequest::EventCallback(bufferevent *bev, int16 events, void *ctx) {
  ProxyRequest *proxy_request = static_cast<ProxyRequest *> (ctx);
  CHECK_NOTNULL(proxy_request);

  if (events & BEV_EVENT_TIMEOUT ||
      events & BEV_EVENT_EOF ||
      events & BEV_EVENT_ERROR) {
    struct evbuffer *input_buf = bufferevent_get_input(bev);
    uint32 len = evbuffer_get_length(input_buf);
    uint8 *input_ptr = evbuffer_pullup(input_buf, len);
    std::string input_str(reinterpret_cast<char *> (input_ptr), len);
    if (input_str.empty()) {
      LOG(ERROR) << "empty response at event: " << events;
      proxy_request->req_->SendError(INTERNAL_SERVER_ERROR,
                                     "Unable to proxy request.");
    } else {
      LOG(INFO) << "sending response at event: " << events;
      proxy_request->req_->CloseConnectionAfterNextWrite();
      proxy_request->req_->SendRaw(input_str);
    }
    delete proxy_request;
    return;
  }
}

std::string ProxyRequest::ExtractValue(const std::string &str,
                                       const std::string &key) {
  std::string rtn;
  const uint64 offset = str.find(key);
  if (offset == std::string::npos) {
    return rtn;
  }
  for (uint32 i = offset + key.length(); i < str.length(); ++i) {
    if (str[i] == '\r' || str[i] == '\n' || str[i] == ' ') {
      break;
    }
    rtn.append(1, str[i]);
  }
  return rtn;
}

bool ProxyRequest::ExtractChunkLength(const std::string &str,
                                      const uint64 offset,
                                      uint64 *chunk_prefix_bytes,
                                      uint64 *chunk_length) {
  std::string cl_str;
  for (uint32 i = offset; i < str.length(); ++i) {
    if (str[i] == '\r' || str[i] == '\n') {
      break;
    }
    cl_str.append(1, str[i]);
  }
  *chunk_prefix_bytes = cl_str.length() + 2;
  return sg::string::FromHexString(cl_str, chunk_length);
}

}  // namespace event
}  // namespace server
}  // namespace http
}  // namespace net
}  // namespace sg
